<!--
@license
Copyright 2016 The TensorFlow Authors. All Rights Reserved.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->

<link rel="import" href="../paper-icon-button/paper-icon-button.html">
<link rel="import" href="../paper-slider/paper-slider.html">
<link rel="import" href="../paper-spinner/paper-spinner-lite.html">
<link rel="import" href="../polymer/polymer.html">
<link rel="import" href="../tf-backend/tf-backend.html">
<link rel="import" href="../tf-card-heading/tf-card-heading.html">
<link rel="import" href="../tf-card-heading/tf-card-heading-style.html">
<link rel="import" href="../tf-card-heading/util.html">
<link rel="import" href="../tf-color-scale/tf-color-scale.html">
<link rel="import" href="../tf-dashboard-common/tensorboard-color.html">
<link rel="import" href="../tf-imports/d3.html">
<link rel="import" href="../tf-imports/lodash.html">

<!--
tf-image-loader loads an individual image from the TensorBoard backend.

Right now it always loads the most recent image. We should add support in the
future for loading older images.
-->
<dom-module id="tf-image-loader">
  <template>
    <tf-card-heading
      tag="[[tag]]"
      run="[[run]]"
      display-name="[[tagMetadata.displayName]]"
      description="[[tagMetadata.description]]"
      color="[[_runColor]]"
    >
      <template is="dom-if" if="[[_hasMultipleSamples]]">
        <div>sample: [[_sampleText]] of [[ofSamples]]</div>
      </template>
      <template is="dom-if" if="[[_hasAtLeastOneStep]]">
        <div class="heading-row">
          <div class="heading-label">
            step <span style="font-weight: bold">[[_toLocaleString(_stepValue)]]</span>
          </div>
          <div class="heading-label heading-right">
            <template is="dom-if" if="[[_currentWallTime]]">
              [[_currentWallTime]]
            </template>
          </div>
          <div class="label right">
            <paper-spinner-lite active hidden$=[[!_isImageLoading]]>
            </paper-spinner-lite>
          </div>
        </div>
      </template>
      <template is="dom-if" if="[[_hasMultipleSteps]]">
        <div>
          <paper-slider
            id="steps"
            immediate-value="{{_stepIndex}}"
            max="[[_maxStepIndex]]"
            max-markers="[[_maxStepIndex]]"
            snaps
            step="1"
            value="{{_stepIndex}}"
          ></paper-slider>
        </div>
      </template>
    </tf-card-heading>

    <button aria-label="Toggle actual size"
            id="main-image-container"
            on-tap="_handleTap"></button>

    <style include="tf-card-heading-style">
      /** Make button a div. */
      button {
        width: 100%;
        display: block;
        background: none;
        border: 0;
        padding: 0;
      }

      /** Firefox: Get rid of dotted line inside button. */
      button::-moz-focus-inner {
        border: 0;
        padding: 0;
      }

      /** Firefox: Simulate Chrome's outer glow on button when focused. */
      button:-moz-focusring {
        outline: none;
        box-shadow: 0px 0px 1px 2px Highlight;
      }

      :host {
        display: block;
        width: 350px;
        height: auto;
        position: relative;
        margin: 0 15px 40px 0;
        --step-slider-knob-color: #424242;
      }

      /** When actual size shown is on, use the actual image width. */
      :host[actual-size] {
        max-width: 100%;
        width: auto;
      }

      :host[actual-size] #main-image-container {
        max-height: none;
        width: auto;
      }

      :host[actual-size] #main-image-container img {
        width: auto;
      }

      paper-spinner-lite {
        width: 14px;
        height: 14px;
        vertical-align: text-bottom;
        --paper-spinner-color: var(--tb-orange-strong);
      }

      #steps {
        height: 15px;
        margin: 0 0 0 -15px;
        /*
         * 31 comes from adding a padding of 15px from both sides of the
         * paper-slider, subtracting 1px so that the slider width aligns
         * with the image (the last slider marker takes up 1px), and
         * adding 2px to account for a border of 1px on both sides of
         * the image. 30 - 1 + 2.
         */
        width: calc(100% + 31px);
        --paper-slider-active-color: var(--step-slider-knob-color);
        --paper-slider-knob-color: var(--step-slider-knob-color);
        --paper-slider-pin-color: var(--step-slider-knob-color);
        --paper-slider-knob-start-color: var(--step-slider-knob-color);
        --paper-slider-knob-start-border-color: var(--step-slider-knob-color);
        --paper-slider-pin-start-color: var(--step-slider-knob-color);
      }

      #main-image-container {
        max-height: 1024px;
        overflow: auto;
      }

      #main-image-container img {
        cursor: pointer;
        display: block;
        image-rendering: -moz-crisp-edges;
        image-rendering: pixelated;
        width: 100%;
        height: auto;
      }

      paper-icon-button {
        color: #2196F3;
        border-radius: 100%;
        width: 32px;
        height: 32px;
        padding: 4px;
      }
      paper-icon-button[selected] {
        background: var(--tb-ui-light-accent);
      }
    </style>
  </template>
  <script>
    "use strict";

    Polymer({
      is: "tf-image-loader",
      properties: {
        run: String,
        tag: String,
        sample: Number,
        ofSamples: Number,
        /** @type {{description: string, displayName: string}} */
        tagMetadata: Object,

        _runColor: {
          type: String,
          computed: '_computeRunColor(run)',
        },
        actualSize: {
          type: Boolean,
          value: false,
          reflectToAttribute: true,  // for CSS
        },
        brightnessAdjustment: {
          type: Number,
          value: 0.5,
        },
        contrastPercentage: {
          type: Number,
          value: 0,
        },

        requestManager: Object,
        _metadataCanceller: {
          type: Object,
          value: () => new tf_backend.Canceller(),
        },
        _imageCanceller: {
          type: Object,
          value: () => new tf_backend.Canceller(),
        },

        // steps: {
        //   width: number,
        //   height: number,
        //   wall_time: Date,
        //   step: number,
        //   url: string,
        // }[]
        _steps: {
          type: Array,
          value: [],
          notify: true,
        },
        _stepIndex: {
          type: Number,
          notify: true,
        },
        _hasAtLeastOneStep: {
          type: Boolean,
          computed: "_computeHasAtLeastOneStep(_steps)",
        },
        _hasMultipleSteps: {
          type: Boolean,
          computed: "_computeHasMultipleSteps(_steps)",
        },
        _stepValue: {
          type: Number,
          computed: "_computeStepValue(_steps, _stepIndex)",
        },
        _currentWallTime: {
          type: Number,
          computed: "_computeCurrentWallTime(_steps, _stepIndex)",
        },
        _maxStepIndex: {
          type: Number,
          computed: "_computeMaxStepIndex(_steps)",
        },
        _sampleText: {
          type: String,
          computed: "_computeSampleText(sample)",
        },
        _hasMultipleSamples: {
          type: Boolean,
          computed: "_computeHasMultipleSamples(ofSamples)",
        },
        _isImageLoading: {
          type: Boolean,
          value: false,
        },
      },
      observers: [
        "reload(run, tag)",
        "_updateImageUrl(_steps, _stepIndex, brightnessAdjustment, contrastPercentage)",
      ],

      _computeRunColor(run) {
        return tf_color_scale.runsColorScale(run);
      },
      _computeHasAtLeastOneStep(steps) {
        return !!steps && steps.length > 0;
      },
      _computeHasMultipleSteps(steps) {
        return !!steps && steps.length > 1;
      },
      _computeStepValue(steps, stepIndex) {
        return steps[stepIndex].step;
      },
      _computeCurrentWallTime(steps, stepIndex) {
        return tf_card_heading.formatDate(steps[stepIndex].wall_time);
      },
      _computeMaxStepIndex(steps) {
        return steps.length - 1;
      },
      _computeSampleText(sample) {
        return `${sample + 1}`;
      },
      _computeHasMultipleSamples(ofSamples) {
        return ofSamples > 1;
      },

      attached() {
        this._attached = true;
        this.reload();
      },
      reload() {
        if (!this._attached) {
          return;
        }
        this._metadataCanceller.cancelAll();
        const router = tf_backend.getRouter();
        const url = tf_backend.addParams(router.pluginRoute('images', '/images'), {
            tag: this.tag,
            run: this.run,
            sample: this.sample,
        });
        const updateSteps = this._metadataCanceller.cancellable(result => {
          if (result.cancelled) {
            return;
          }
          const data = result.value;
          const steps = data.map(this._createStepDatum.bind(this));
          this.set("_steps", steps);
          this.set("_stepIndex", steps.length - 1);
        });
        this.requestManager.request(url).then(updateSteps);
      },
      _createStepDatum(imageMetadata) {
        let url = tf_backend.getRouter().pluginRoute('images', '/individualImage');
        // Include wall_time just to disambiguate the URL and force
        // the browser to reload the image when the URL changes. The
        // backend doesn't care about the value.
        url = tf_backend.addParams(url, {ts: imageMetadata.wall_time});
        url += '&' + imageMetadata.query;

        return {
          width: imageMetadata.width,
          height: imageMetadata.height,
          // The wall time within the metadata is in seconds. The Date
          // constructor accepts a time in milliseconds, so we multiply by 1000.
          wall_time: new Date(imageMetadata.wall_time * 1000),
          step: imageMetadata.step,
          url,
        };
      },
      _updateImageUrl(steps, stepIndex, brightnessAdjustment, contrastPercentage) {
        // We manually change the image URL (instead of binding to the
        // image's src attribute) because we would like to manage what
        // happens when the image starts and stops loading.
        if (!steps.length) {
          return;
        }

        const img = new Image();
        this._imageCanceller.cancelAll();
        img.onload = img.onerror = this._imageCanceller.cancellable(result => {
          if (result.cancelled) {
            return;
          }
          const mainImageContainer = this.$$("#main-image-container");
          mainImageContainer.innerHTML = "";
          Polymer.dom(mainImageContainer).appendChild(img);
          this.set("_isImageLoading", false);
        }).bind(this);

        img.style.filter = `contrast(${contrastPercentage}%) `;
        img.style.filter += `brightness(${brightnessAdjustment})`;

        // Load the new image.
        this.set("_isImageLoading", true);
        img.src = steps[stepIndex].url;
      },
      _handleTap(e) {
        this.set('actualSize', !this.actualSize);
      },
      _toLocaleString(number) {
        // Shows commas (or locale-appropriate punctuation) for large numbers.
        return number.toLocaleString();
      },
    });
  </script>
</dom-module>
